بیاموزید چگونه مرزهای خطای ریاکت (React Error Boundaries) را با هوکها پیادهسازی کنید تا خطاهای بارگذاری منابع را به زیبایی مدیریت کرده و تجربه کاربری و پایداری برنامه را بهبود بخشید.
بارگذاری مقاوم منابع در ریاکت: تسلط بر مرزهای خطا (Error Boundaries) با هوکها
در برنامههای وب مدرن، بارگذاری ناهمزمان منابع یک روش رایج است. چه دریافت داده از یک API باشد، چه بارگذاری تصاویر یا ایمپورت کردن ماژولها، مدیریت خطاهای احتمالی در حین بارگذاری منابع برای یک تجربه کاربری روان حیاتی است. مرزهای خطای ریاکت (React Error Boundaries) مکانیزمی برای دریافت خطاهای جاوا اسکریپت در هر جای درخت کامپوننت فرزند خود فراهم میکنند، آن خطاها را ثبت کرده و به جای از کار افتادن کل برنامه، یک رابط کاربری جایگزین (fallback UI) نمایش میدهند. این مقاله به بررسی نحوه استفاده مؤثر از مرزهای خطا در ترکیب با هوکهای ریاکت برای مدیریت خطاهای بارگذاری منابع میپردازد.
درک مرزهای خطا (Error Boundaries)
قبل از ریاکت ۱۶، خطاهای جاوا اسکریپت رسیدگینشده در حین رندر کامپوننتها، وضعیت داخلی ریاکت را خراب میکرد و باعث خطاهای نامفهوم در رندرهای بعدی میشد. مرزهای خطا با عمل کردن به عنوان بلوکهای دریافتکننده کلی برای خطاهایی که در کامپوننتهای فرزندشان رخ میدهد، این مشکل را حل میکنند. آنها کامپوننتهای ریاکتی هستند که یکی یا هر دوی متدهای چرخه حیات زیر را پیادهسازی میکنند:
static getDerivedStateFromError(error): این متد استاتیک پس از بروز خطا توسط یک کامپوننت فرزند فراخوانی میشود. این متد خطای رخداده را به عنوان آرگومان دریافت میکند و مقداری را برای بهروزرسانی وضعیت کامپوننت برمیگرداند.componentDidCatch(error, info): این متد چرخه حیات پس از بروز خطا توسط یک کامپوننت فرزند فراخوانی میشود. این متد خطای رخداده را به عنوان آرگومان و همچنین یک شی حاوی اطلاعات در مورد اینکه کدام کامپوننت خطا را ایجاد کرده است، دریافت میکند. میتوانید از آن برای ثبت اطلاعات خطا استفاده کنید.
نکته مهم این است که مرزهای خطا فقط خطاهای فاز رندر، متدهای چرخه حیات و سازندههای کل درخت زیرمجموعه خود را دریافت میکنند. آنها خطاهای موارد زیر را دریافت نمیکنند:
- کنترلکنندههای رویداد (event handlers) (در بخش زیر بیشتر بیاموزید)
- کد ناهمزمان (مانند کالبکهای
setTimeoutیاrequestAnimationFrame) - رندر سمت سرور (Server-side rendering)
- خطاهایی که در خود مرز خطا رخ میدهند (به جای فرزندان آن)
مرزهای خطا و هوکهای ریاکت: ترکیبی قدرتمند
در حالی که کامپوننتهای کلاسی به طور سنتی برای پیادهسازی مرزهای خطا استفاده میشدند، هوکهای ریاکت رویکردی مختصرتر و تابعی ارائه میدهند. ما میتوانیم یک هوک قابل استفاده مجدد useErrorBoundary ایجاد کنیم که منطق مدیریت خطا را در خود کپسوله کرده و راهی راحت برای پیچیدن کامپوننتهایی که ممکن است در حین بارگذاری منابع خطا ایجاد کنند، فراهم میکند.
ایجاد یک هوک سفارشی useErrorBoundary
در اینجا نمونهای از یک هوک useErrorBoundary آورده شده است:
import { useState, useCallback } from 'react';
function useErrorBoundary() {
const [error, setError] = useState(null);
const resetError = useCallback(() => {
setError(null);
}, []);
const captureError = useCallback((e) => {
setError(e);
}, []);
const ErrorBoundary = useCallback(({ children, fallback }) => {
if (error) {
return fallback ? fallback : An error occurred: {error.message || String(error)};
}
return children;
}, [error]);
return { ErrorBoundary, captureError, error, resetError };
}
export default useErrorBoundary;
توضیح:
useState: ما ازuseStateبرای مدیریت وضعیت خطا استفاده میکنیم. در ابتدا خطا را رویnullتنظیم میکند.useCallback: ما ازuseCallbackبرای مموایز کردن توابعresetErrorوcaptureErrorاستفاده میکنیم. این یک روش خوب برای جلوگیری از رندرهای مجدد غیرضروری است اگر این توابع به عنوان پراپ به کامپوننتهای دیگر منتقل شوند.- کامپوننت
ErrorBoundary: این یک کامپوننت تابعی است که باuseCallbackایجاد شده وchildrenو یک پراپ اختیاریfallbackرا میگیرد. اگر خطایی در وضعیت وجود داشته باشد، یا کامپوننتfallbackارائهشده را رندر میکند یا یک پیام خطای پیشفرض را نمایش میدهد. در غیر این صورت، فرزندان را رندر میکند. این به عنوان مرز خطای ما عمل میکند. آرایه وابستگی `[error]` تضمین میکند که با تغییر وضعیت `error`، دوباره رندر شود. - تابع
captureError: این تابع برای تنظیم وضعیت خطا استفاده میشود. شما این تابع را درون یک بلوکtry...catchهنگام بارگذاری منابع فراخوانی خواهید کرد. - تابع
resetError: این تابع وضعیت خطا را پاک میکند و به کامپوننت اجازه میدهد تا فرزندان خود را دوباره رندر کند (و به طور بالقوه بارگذاری منابع را مجدداً تلاش کند).
پیادهسازی بارگذاری منابع با مدیریت خطا
حال، بیایید ببینیم چگونه از این هوک برای مدیریت خطاهای بارگذاری منابع استفاده کنیم. یک کامپوننت را در نظر بگیرید که دادههای کاربر را از یک API دریافت میکند:
import React, { useState, useEffect } from 'react';
import useErrorBoundary from './useErrorBoundary';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const { ErrorBoundary, captureError, error, resetError } = useErrorBoundary();
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (e) {
captureError(e);
}
};
fetchData();
}, [userId, captureError]);
if (error) {
return (
Failed to load user data. {user.name}
Email: {user.email}
{/* Other user details */}توضیح:
- ما هوک
useErrorBoundaryرا ایمپورت میکنیم. - ما هوک را فراخوانی میکنیم تا کامپوننت
ErrorBoundary، تابعcaptureError، وضعیتerrorو تابعresetErrorرا دریافت کنیم. - درون هوک
useEffect، ما فراخوانی API را در یک بلوکtry...catchقرار میدهیم. - اگر در حین فراخوانی API خطایی رخ دهد، ما
captureError(e)را برای تنظیم وضعیت خطا فراخوانی میکنیم. - اگر وضعیت
errorتنظیم شده باشد، ما کامپوننتErrorBoundaryرا رندر میکنیم. ما یک پراپ سفارشیfallbackارائه میدهیم که یک پیام خطا و یک دکمه «تلاش مجدد» را نمایش میدهد. با کلیک بر روی دکمه،resetErrorبرای پاک کردن وضعیت خطا فراخوانی میشود، که باعث رندر مجدد و تلاش دوباره برای دریافت داده میشود. - اگر خطایی رخ نداده باشد و دادههای کاربر بارگذاری شده باشد، ما جزئیات پروفایل کاربر را رندر میکنیم.
مدیریت انواع مختلف خطاهای بارگذاری منابع
انواع مختلف خطاهای بارگذاری منابع ممکن است به استراتژیهای مدیریتی متفاوتی نیاز داشته باشند. در اینجا برخی از سناریوهای رایج و نحوه رسیدگی به آنها آورده شده است:
خطاهای شبکه
خطاهای شبکه زمانی رخ میدهند که کلاینت قادر به اتصال به سرور نباشد (مثلاً به دلیل قطعی شبکه یا از کار افتادن سرور). مثال بالا قبلاً خطاهای شبکه پایه را با استفاده از `response.ok` مدیریت میکند. ممکن است بخواهید تشخیص خطای پیچیدهتری اضافه کنید، به عنوان مثال:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
// Consider adding specific error code handling
if (response.status === 404) {
throw new Error("User not found");
} else if (response.status >= 500) {
throw new Error("Server error. Please try again later.");
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error.message === 'Failed to fetch') {
// Likely a network error
captureError(new Error('Network error. Please check your internet connection.'));
} else {
captureError(error);
}
}
در این حالت، میتوانید پیامی به کاربر نمایش دهید که نشاندهنده مشکل اتصال به شبکه است و به او پیشنهاد دهید اتصال اینترنت خود را بررسی کند.
خطاهای API
خطاهای API زمانی رخ میدهند که سرور یک پاسخ خطا برمیگرداند (مانند 400 Bad Request یا 500 Internal Server Error). همانطور که در بالا نشان داده شد، میتوانید `response.status` را بررسی کرده و این خطاها را به طور مناسب مدیریت کنید.
خطاهای تجزیه (Parsing) داده
خطاهای تجزیه داده زمانی رخ میدهند که پاسخ از سرور در فرمت مورد انتظار نباشد و نتوان آن را تجزیه کرد (مانند JSON نامعتبر). میتوانید این خطاها را با قرار دادن فراخوانی response.json() در یک بلوک try...catch مدیریت کنید:
//Inside the fetchData function
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error instanceof SyntaxError) {
captureError(new Error('Failed to parse data from server.'));
} else {
captureError(error);
}
}
خطاهای بارگذاری تصویر
برای بارگذاری تصویر، میتوانید از رویداد onError در تگ <img> استفاده کنید:
function MyImage({ src, alt }) {
const { ErrorBoundary, captureError } = useErrorBoundary();
const [imageLoaded, setImageLoaded] = useState(false);
const handleImageLoad = () => {
setImageLoaded(true);
};
const handleImageError = (e) => {
captureError(new Error(`Failed to load image: ${src}`));
};
return (
Failed to load image.